/*******************************************************************************
 * Copyright (c) 2011 Irineu Martins de Lima Moura (imlm2).
 * This file is part of JBeat.
 * 
 *     JBeat is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     JBeat is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with JBeat.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Irineu Martins de Lima Moura (imlm2) - initial API and implementation
 ******************************************************************************/
package br.ufpe.cin.imlm2.jbeat.soot;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import soot.PackManager;
import soot.SootClass;
import soot.SootMethod;
import soot.Transform;
import soot.options.Options;
import br.ufpe.cin.imlm2.jbeat.IExceptionFlowAnalysis;
import br.ufpe.cin.imlm2.jbeat.soot.common.SootClassResolver;
import br.ufpe.cin.imlm2.jbeat.soot.icpoints.TrapInspector;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.generation.GeneratesCalculator;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.generation.RaisesCalculator;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.propagation.PropagatesCalculator;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.ThrowableSetTag;
import br.ufpe.cin.imlm2.jbeat.soot.traps.EmptyTrapRemover;
import br.ufpe.cin.imlm2.util.Naming;

public class ExceptionAnalysisImpl implements IExceptionFlowAnalysis {

	private static final ExceptionAnalysisRunner RUNNER = new ExceptionAnalysisRunner();

	private List<SootClass> targetSet;

	private ExceptionAnalysisImpl(List<SootClass> targetSet) {
		super();
		this.targetSet = targetSet;
	}


	/**
	 * This class encapsulates all the details necessary to setup the <br>
	 * arguments for the exception flow analysis. <br>
	 * It uses the builder pattern so that you may chain calls to setup the wanted arguments. <br>
	 * A typical usage example would be: <br><br>
	 * <code>
	 * String mainClass = ...; <br>
	 * String classpath = ...; <br>
	 * ExceptionAnalysisRunner ear = ExceptionAnalysisImpl.getRunner(); <br>
	 * ear.preciseCallGraph(true).excludeClass("java.lang.String");  <br>
	 * ExceptionAnalysisImpl analysis = ear.runAnalysis(mainClass,classpath);
	 * </code>
	 * 
	 * @author irineu
	 *
	 */
	public static class  ExceptionAnalysisRunner{
		private boolean spark = false;
		private boolean trapInspection = true;
		private boolean allReachable = true;
		private boolean clinits = false;
		private boolean autoGenerated = false;
		private List<String> classesExcludeList = new ArrayList<String>();
		private List<String> packagesExcludeList = new ArrayList<String>();
		private boolean analyzeRT = true;
		private boolean withReflection = false;
		private boolean rtLibs = true;
		private List<String> classpath = new ArrayList<String>();

		/**
		 * Runs the analysis over the given main class and all reachables classes in the classpath. <br>
		 * Each entry in the classpath must be separated according to the client's OS specific file separator. <br>
		 * The objects returned by this method may safely be cast to <code>ExceptionAnalysisImpl</code> <br>
		 * If a class needed for the analysis is not found then a RuntimeException will be thrown. <br>
		 * @param mainClass
		 * @param classpath
		 * @return
		 */
		public IExceptionFlowAnalysis runAnalysis(String mainClass, String classpath){
			Options ops = Options.v();
			ops.set_whole_program(true);
			ops.set_app(true);
			if(spark) ops.setPhaseOption("cg.spark", "enabled:true");
			else ops.setPhaseOption("cg.cha", "enabled:true");
			ops.setPhaseOption("cg", "implicit-entry:false");
			ops.set_allow_phantom_refs(true);

			if(allReachable) ops.setPhaseOption("cg", "all-reachable:true");
			else ops.setPhaseOption("cg", "all-reachable:false");

			if(!this.withReflection) this.packagesExcludeList.add("java.lang.reflect.*");

			EmptyTrapRemover etr = new EmptyTrapRemover();
			PackManager.v().getPack("wjtp").add(new Transform("wjtp.etr", etr));

			if(trapInspection) {
				TrapInspector ti = new TrapInspector(this.classesExcludeList,
						this.packagesExcludeList, this.rtLibs);
				PackManager.v().getPack("wjtp").add(new Transform("wjtp.ti", ti));
			}

			if(this.analyzeRT){
				GeneratesCalculator gc = new GeneratesCalculator(this.classesExcludeList,
						this.packagesExcludeList, this.rtLibs);
				PackManager.v().getPack("wjtp").add(new Transform("wjtp.gc", gc));
			}
			
			RaisesCalculator rc = new RaisesCalculator(this.classesExcludeList,
					this.packagesExcludeList, this.rtLibs);
			PackManager.v().getPack("wjtp").add(new Transform("wjtp.rc", rc));
			PropagatesCalculator pc = new PropagatesCalculator(this.classesExcludeList,
					this.packagesExcludeList, this.rtLibs,clinits, autoGenerated);
			PackManager.v().getPack("wjtp").add(new Transform("wjtp.pc", pc));

			String[] args = {mainClass,"-f","J","-cp", classpath};
			soot.Main.main(args);

			Collection<SootClass> classes = SootClassResolver.resolveClasses(classesExcludeList, 
					packagesExcludeList, rtLibs);
			return new ExceptionAnalysisImpl(new ArrayList<SootClass>(classes));
		}


		/**
		 * Use the more precise but slower CallGraph construction algorithm or not. <br>
		 * Setting this to true will likely decrease the number of possible exceptions <br>
		 * generated by method found by the analysis <br>
		 * Default value is false
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner preciseCallGraph(boolean b){
			this.spark = b;
			return this;
		}

		/**
		 * Use the handler inspection algorithm as described by Fu & Ryder or not
		 * Defaults to true
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner trapInspection(boolean b){
			this.trapInspection  = b;
			return this;
		}

		/** 
		 * Consider unreachable methods in the classpath when constructing the callgraph or not
		 * Defaults to true
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner allReachableEntryPoints(boolean b){
			this.allReachable  = b;
			return this;
		}

		/**
		 * Consider static initializers in the callgraph or not.
		 * Setting this to true most likely will drastically increase the number of possible
		 * exceptions generated by a method found by the analysis, thus it is recommended to use
		 * the default values. 
		 * Defaults to false
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner staticInitializars(boolean b){
			this.clinits = b;
			return this;
		}

		/**
		 * Consider additional methods generated by the compiler or not
		 * Defaults to false
		 * Most likely this WILL NOT exclude all possible auto-generated methods.
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner autoGeneratedMethods(boolean b){
			this.autoGenerated = b;
			return this;
		}

		/**
		 * Don't consider classes with names passed as parameters in the analysis.
		 * Null,empty and repeated Strings are ignored
		 * @param fullyQualifiedClassesNames
		 * @return
		 */
		public ExceptionAnalysisRunner excludeClasses(List<String> fullyQualifiedClassesNames){
			for (String string : fullyQualifiedClassesNames) {
				this.excludeType(string);
			}
			return this;
		}

		/**
		 * Don't consider the class with name passed as parameter in the analysis.
		 * If the name passed as parameter is not a valid JVM fully classified type name then it will
		 * be discarded.
		 * Null,empty and repeated Strings are ignored
		 * @param fullyQualifiedTypeName
		 * @return
		 */
		public ExceptionAnalysisRunner excludeType(String fullyQualifiedTypeName){
			if(fullyQualifiedTypeName != null && !fullyQualifiedTypeName.isEmpty() && 
					!this.classesExcludeList.contains(fullyQualifiedTypeName) &&
					Naming.isValidJVMName(fullyQualifiedTypeName)) 
				this.classesExcludeList.add(fullyQualifiedTypeName);
			return this;
		}

		/**
		 * Don't consider the classes from the given packaged in the analysis
		 * Null,empty and repeated Strings are ignored
		 * @param fullyQualifiedPackageName
		 * @return
		 */
		public ExceptionAnalysisRunner excludePackage(String fullyQualifiedPackageName){
			if(fullyQualifiedPackageName != null && !fullyQualifiedPackageName.isEmpty() &&
					!this.packagesExcludeList.contains(fullyQualifiedPackageName) &&
					Naming.isValidJVMName(fullyQualifiedPackageName)) 
				this.packagesExcludeList.add(fullyQualifiedPackageName);
			return this;
		}

		/**
		 * Consider exceptions thrown by the runtime? (i.e. ArrayIndexOutOfBounds,NullPointer, etc)
		 * Defaults to <code>true</code>
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerThrownByRuntime(boolean b){
			this.analyzeRT = b;
			return this;
		}

		/**
		 * Consider exceptions thrown by methods in classes belonging to the java.lang.reflect?
		 * Defaults to <code>false</code>
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerReflectionPackages(boolean b){
			this.withReflection = b;
			return this;
		}

		/**
		 * Consider exceptions thrown by methods in the Java Runtime Libraries?
		 * Defaults to <code>false</code>
		 * @param b
		 * @return
		 */
		public ExceptionAnalysisRunner considerRuntimeLibraries(boolean b){
			this.rtLibs = b;
			return this;
		}
	}

	/**
	 * Returns the  ExceptionAnalysisRunner object necessary to setup and run this analysis.
	 * @return
	 */
	public static ExceptionAnalysisRunner getRunner(){
		return RUNNER;
	}

	public Collection<SootClass> getSootClassExceptionsThrownBy(String fullyQualifiedClassName,
			String fullyQualifiedMethName) throws Exception {
		for (SootClass c : this.targetSet) {
			if(c.equals(fullyQualifiedClassName)){
				try{
					SootMethod m = c.getMethod(fullyQualifiedMethName);
					return this.getExceptionsThrownBy(m);					
				}catch(RuntimeException e){
					throw new MethodNotFoundException("Method with name " + fullyQualifiedMethName +
							" wasn't found in type " + fullyQualifiedClassName);
				}
			}
		}
		throw new TypeNotFoundException("Type with name + " + fullyQualifiedClassName + " wasn't found");
	}

	public Collection<SootClass>  getExceptionsThrownBy(SootMethod m) {
		ThrowableSetTag tag = (ThrowableSetTag) m.getTag(ThrowableSetTag.TAG_NAME);
		if (tag == null) {
			throw new RuntimeException("ThrowableSetTag wasn't found for method " + m.getSignature() + 
					". This is probably a bug or you are trying to retrieve the exceptions from a method " +
			"not considered in the analysis.");
		}
		HashSet<SootClass> classes = new HashSet<SootClass>(tag.getThrowableSet());
		return classes;
	}

	public Collection<String> getExceptionsThrownBy(String fullyQualifiedClassName,
			String fullyQualifiedMethName) throws Exception{
		Collection<SootClass> classes = this.getSootClassExceptionsThrownBy(fullyQualifiedClassName,
				fullyQualifiedMethName);
		Collection<String> classesNames = new ArrayList<String>();
		for (SootClass s : classes) {
			classesNames.add(s.getName());
		}
		return classesNames;
	}


	public static void main(String[] args) {
		String mainClass = "br.ufpe.cin.imlm2.stubs.Stub";
		String classpath = "/C:/Users/Irineu/workspace/esat/src;" +
		"/C:/Program Files/Java/jdk1.6.0_21/jre/lib/charsets.jar;/C:/esat/src;" +
		"/C:/Program Files/Java/jdk1.6.0_21/jre/lib/ext/sunjce_provider.jar;" +
		"/C:/Program Files/Java/jdk1.6.0_21/jre/lib/rt.jar;" +
		"/C:/Program Files/Java/jdk1.6.0_21/jre/lib/jce.jar;lib/polyglot.jar;" +
		"/C:/Users/Irineu/workspace/esat/bin/;/C:/Program Files/Java/jdk1.6.0_21/jre/lib/resources.jar;" +
		"/C:/Program Files/Java/jdk1.6.0_21/jre/lib/ext/dnsns.jar";
		ExceptionAnalysisRunner runner = ExceptionAnalysisImpl.getRunner();
		IExceptionFlowAnalysis analysis = runner.excludePackage("java.lang.reflect").
		preciseCallGraph(true).allReachableEntryPoints(true).runAnalysis(mainClass, classpath);
	}


}
